// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE

import * as CodeMirror from "codemirror";

var Pos = CodeMirror.Pos;

function forEach(arr, f) {
    for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
}

function arrayContains(arr, item) {
    if (!Array.prototype.indexOf) {
        var i = arr.length;
        while (i--) {
            if (arr[i] === item) {
                return true;
            }
        }
        return false;
    }
    return arr.indexOf(item) != -1;
}

function scriptHint(editor, keywords, getToken, options) {
    // Find the token at the cursor
    var cur = editor.getCursor(), token = getToken(editor, cur);
    if (/\b(?:string|comment)\b/.test(token.type)) return;
    var innerMode = CodeMirror.innerMode(editor.getMode(), token.state);
    if (innerMode.mode.helperType === "json") return;
    token.state = innerMode.state;

    // If it's not a 'word-style' token, ignore the token.
    if (!/^[\w$_]*$/.test(token.string)) {
        token = {
            start: cur.ch, end: cur.ch, string: "", state: token.state,
            type: token.string == "." ? "property" : null
        };
    } else if (token.end > cur.ch) {
        token.end = cur.ch;
        token.string = token.string.slice(0, cur.ch - token.start);
    }

    var tprop = token;
    // If it is a property, find out what it is a property of.
    while (tprop.type == "property") {
        tprop = getToken(editor, Pos(cur.line, tprop.start));
        if (tprop.string != ".") return;
        tprop = getToken(editor, Pos(cur.line, tprop.start));
        if (!context) var context = [];
        context.push(tprop);
    }
    const list = token.string == null || token.string === "" ? [] : getCompletions(token, context, keywords, options);
    console.info("scriptHint", token, list);
    return {
        list: list,
        from: Pos(cur.line, token.start),
        to: Pos(cur.line, token.end)
    };
}

function javascriptHint(editor, options) {
    return scriptHint(editor, javascriptKeywords,
        function (e, cur) {
            return e.getTokenAt(cur);
        },
        options);
};
CodeMirror.registerHelper("hint", "javascript", javascriptHint);

function getCoffeeScriptToken(editor, cur) {
    // This getToken, it is for coffeescript, imitates the behavior of
    // getTokenAt method in javascript.js, that is, returning "property"
    // type and treat "." as indepenent token.
    var token = editor.getTokenAt(cur);
    if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') {
        token.end = token.start;
        token.string = '.';
        token.type = "property";
    } else if (/^\.[\w$_]*$/.test(token.string)) {
        token.type = "property";
        token.start++;
        token.string = token.string.replace(/\./, '');
    }
    return token;
}

function coffeescriptHint(editor, options) {
    return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);
}

CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint);

var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
    "toUpperCase toLowerCase split concat match replace search").split(" ");
var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " +
    "lastIndexOf every some filter forEach map reduce reduceRight ").split(" ");
var funcProps = "prototype apply call bind".split(" ");
var javascriptKeywords = ("break case catch class const continue debugger default delete do else export extends false finally for function " +
    "if in import instanceof new null return super switch this throw true try typeof var void while with yield " +
    "text_1 text_2 text_3 text_4 text_5 text_center_1 movie_1 movie_2 movie_3 movie_1_left_pic movie_1_vertical_pic movie_1_vertical_pic_blur movie_3_marquee pic_1 pic_2 pic_3 pic_1_full pic_3_square pic_1_card icon_1_search icon_2 icon_2_round icon_4 icon_4_card icon_small_3 icon_small_4 icon_round_4 icon_round_small_4 long_text rich_text avatar x5_webview_single line line_blank blank_block big_blank_block flex_button scroll_button card_pic_2 card_pic_1 card_pic_2_2 card_pic_2_2_left input big_big_blank_block " +
    "href title JSON parse stringify Text Html getResCode() request fetch parseDom parseDomForHtml parseDomForArray MY_URL MY_PARAMS MY_RULE MY_ACCOUNT MY_ACCOUNT setResult withHeaders method headers charst application/json rule lazyRule writeFile fileExist saveImage setPageTitle getColTypes getRuleCount getLastRules refreshPage refreshX5WebView batchFetch setLastChapterRule getParam log col_type immersiveTheme# ignoreVideo=true# isVideo=true# ignoreImg=true# noRecordHistory noHistory redirect User-Agent Referer Content-Type getCryptoJS base64Decode base64Encode escape decodeURIComponent hiker:// hiker://empty hiker://page/ hiker://files/ bookmark web:// download history collection setting javascript toast:// confirm:// editFile:// x5Play:// x5WebView:// titleVisible extra defaultValue onChange addListener onClose onRefresh getAppVersion exports require clipboard toString toString(()=>{\\n    \\n}) clearVar showLoading hideLoading confirm content cancel setItem getItem clearItem saveFile readFile canBack ignoreMusic=true# isMusic=true# MOBILE_UA PC_UA select:// autoPage# textSize x5Rule:// options click getCode pdfa pdfh refresh listen error true false fori forof forin pics:// pushit pushLine pushBigBlock pushRich fy_bridge_app js:init varPd varPdfh varPdfa parsePaste toHex: true pre# noPre# deleteFile lineVisible ignoreM3U8# getVar putVar blockRules getPastes sharePaste evalPrivateJS getPrivateJS isLogin newWindow: true func://background func:// background windowId gradient: true md5 subtitle requireCache fetchCache varjson hexToBytes cacheM3u8 batchCacheM3u8 danmu webRule:// setPagePicUrl post postRequest buildUrl deleteCache json:parse json:parse2 json:stringify putMyVar getMyVar initConfig config fullTheme# clearMyVar").split(" ");
var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
    "if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");

function forAllProps(obj, callback) {
    if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {
        for (var name in obj) callback(name)
    } else {
        for (var o = obj; o; o = Object.getPrototypeOf(o))
            Object.getOwnPropertyNames(o).forEach(callback)
    }
}

function getCompletions(token, context, keywords, options) {
    var found = [], start = token.string, global = options && options.globalScope || window;

    function maybeAdd(str) {
        if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);
    }

    function gatherCompletions(obj) {
        if (typeof obj == "string") forEach(stringProps, maybeAdd);
        else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
        else if (obj instanceof Function) forEach(funcProps, maybeAdd);
        forAllProps(obj, maybeAdd)
    }

    if (context && context.length) {
        // If this is a property, see if it belongs to some object we can
        // find in the current environment.
        var obj = context.pop(), base;
        if (obj.type && obj.type.indexOf("variable") === 0) {
            if (options && options.additionalContext)
                base = options.additionalContext[obj.string];
            if (!options || options.useGlobalScope !== false)
                base = base || global[obj.string];
        } else if (obj.type == "string") {
            base = "";
        } else if (obj.type == "atom") {
            base = 1;
        } else if (obj.type == "function") {
            if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
                (typeof global.jQuery == 'function'))
                base = global.jQuery();
            else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))
                base = global._();
        }
        while (base != null && context.length)
            base = base[context.pop().string];
        if (base != null) gatherCompletions(base);
    } else {
        // If not, just look in the global object, any local scope, and optional additional-context
        // (reading into JS mode internals to get at the local and global variables)
        for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
        for (var c = token.state.context; c; c = c.prev)
            for (var v = c.vars; v; v = v.next) maybeAdd(v.name)
        for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
        if (options && options.additionalContext != null)
            for (var key in options.additionalContext)
                maybeAdd(key);
        if (!options || options.useGlobalScope !== false)
            gatherCompletions(global);
        forEach(keywords, maybeAdd);
    }
    return found;
}
